package com.dude.search;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDMatch;
import org.opencv.core.MatOfKeyPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.highgui.Highgui;
import org.opencv.highgui.VideoCapture;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.DMatch;
import org.opencv.features2d.DescriptorMatcher;
import org.opencv.features2d.FeatureDetector;
import org.opencv.features2d.DescriptorExtractor;
import org.opencv.features2d.KeyPoint;

import com.dude.R;
import com.dude.interfaces.UINotifier;
import com.dude.objects.GenericPair;
import com.dude.screens.Settings;

import android.content.Context;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.net.Uri;
import android.util.Log;
import android.view.SurfaceHolder;

public class ObjectDetectionView extends CameraView implements UINotifier
{
	private static final String TAG = "obj detect view";
	// ====================== sound alert assets ===========================
	private SoundPool soundPool;
	private float volume;
	private boolean useSound;
	
	private MediaPlayer[] mps;
	
	// ====================== matching parameters ===========================
	public static boolean isSym = true;
	
	private static float knnMatchingThreshold = 0.6f;
	private static int hessianThreshold = 1000;
	private static int numOfOctaves = 3;
	private static int octaveLayers = 3;

	// =====================================================================
	private Bitmap refBmp;
	private Bitmap frameBmp;
	private File threshFile;
	private File tempDir;
	
	private FeatureDetector detector;
	private DescriptorExtractor extractor;
	private Mat objectMat;
	private MatOfKeyPoint keyPoints;
	private Mat descriptor;
	private Mat imgCap;
	private Mat imgCapColor;
	private MatOfKeyPoint keyPointsCap;
	private Mat descriptorCap;
	private DescriptorMatcher matcher;
	private ArrayList<MatOfDMatch> matches;

	private DMatch[] point;
	private ArrayList<DMatch> goods;
	private ArrayList<DMatch> goods_2 = new ArrayList<DMatch>();
	private ArrayList<PicStats> objects;
	private PicStats cur;
	private int place=0;
	private int size=0;
	

	// ====================== constructor ============================
	
	public ObjectDetectionView(Context context, ArrayList<GenericPair<Bitmap, String>> bitmaps)
	{
		super(context);
		Log.i(TAG, "entered");
		
//		initSound(context);
		initSound(context, bitmaps.size());
		objects = new ArrayList<PicStats>();
		
		detector = FeatureDetector.create(FeatureDetector.SURF);
		extractor = DescriptorExtractor.create(DescriptorExtractor.SURF);
		matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE);

		createDetectorSettingsFile(context);
		detector.read(threshFile.getAbsolutePath());
		
		PicStats p;
		
		for (int i = 0; i < bitmaps.size(); i++)
		{
			loadRefrenceObject(context, bitmaps.get(i).getFirst());
//			int soundId = loadSound(context, bitmaps.get(i).getSecond());
//			p = new PicStats(i, soundId);
			loadSound(context, bitmaps.get(i).getSecond(), i);
			p = new PicStats(i, i);
			detector.detect(objectMat, p.getKeyPoints());
			extractor.compute(objectMat,p.getKeyPoints(), p.getDescriptor());
			objects.add(p);
		}
		
		size=objects.size();
		objects.get(size-1).setCheckBest(true);
		cur=objects.get(0);
		
		
		imgCap = new Mat();
		imgCapColor = new Mat();
		
		keyPointsCap = new MatOfKeyPoint();
		descriptorCap = new Mat();
		
		matches = new ArrayList<MatOfDMatch>();
		goods = new ArrayList<DMatch>(100);
		
		Log.i(TAG, "created");
	}
	
	public void surfaceDestroyed(SurfaceHolder holder)
	{
		finished = true;
		super.surfaceDestroyed(holder);

		if (threshFile != null && threshFile.exists())
			threshFile.delete();
		if (tempDir != null && tempDir.exists())
			tempDir.delete();
		
		if (useSound)
		{
//			new Thread (new Runnable(){
//
//				@Override
//				public void run() {
//					soundPool.release();
//					soundPool = null;
//				}
//			}).start();
			
			if (mps != null)
				for (MediaPlayer mp : mps)
					if (mp != null)
					{
						mp.reset();
						mp.release();
						mp = null;
					}
			mps = null;
		}
		Log.i(TAG, "surface destroyed");
	}
		
	// ===========================================
	
	private void getCurrentObjectFast() 
	{
		if (++place <size){
			cur=objects.get(place);
			keyPoints=cur.getKeyPoints();
			descriptor=cur.getDescriptor();
		}else{
			place=0;
			cur=objects.get(place);
			keyPoints=cur.getKeyPoints();
			descriptor=cur.getDescriptor();
		}
	}
	
//	private void getCurrentObject() {		
//		if (cur.isCheckBest() && cur.getTurnsLeft()==0){
//			cur.setTurnsLeft(-1);
//			if (++place <size){
//				cur=objects.get(place);
//				keyPoints=cur.getKeyPoints();
//				descriptor=cur.getDescriptor();
//			}else{
//				place=0;
//				cur=objects.get(place);
//				keyPoints=cur.getKeyPoints();
//				descriptor=cur.getDescriptor();
//			}
//			return;
//		}
//		if (cur.getTurnsLeft()>0){
//			cur.setTurnsLeft(cur.getTurnsLeft()-1);
//		}else{
//			if (cur.isCheckBest() && cur.getTurnsLeft()==-1){
//				cur.setTurnsLeft(0);
//				cur.setCheckBest(false);
//				int max=0;
//				place = 0;
//				PicStats p;
//				for (int i=0;i<size;i++){
//					p=objects.get(i);
//					if ((p.getNumOfMatches()>2) && (p.getNumOfMatches()>max)){
//						cur=p;
//						place=i;
//						max=p.getNumOfMatches();
//					}
//				}
//				if (max==0){
//					cur=objects.get(0);
//					place=0;
//					cur.setTurnsLeft(0);
//					cur.setCheckBest(true);
//				}else{
//					cur.setTurnsLeft(5);
//					cur.setCheckBest(true);
//				}
//				
//			}
//			else{
//				if (++place <size){
//					cur=objects.get(place);
//					keyPoints=cur.getKeyPoints();
//					descriptor=cur.getDescriptor();
//				}else{
//					place=0;
//					cur=objects.get(place);
//					keyPoints=cur.getKeyPoints();
//					descriptor=cur.getDescriptor();
//				}
//			}
//		}
//	}
	
	//compares all the photos against every frame and draws one circle over the imddle of the best matches
	protected Bitmap processFrameMultiPic(VideoCapture capture) 
	{
		int maxPoints=0;
		List<KeyPoint> bestKeyPoints=new ArrayList<KeyPoint>();
		capture.retrieve(imgCapColor, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
		capture.retrieve(imgCap, Highgui.CV_CAP_ANDROID_GREY_FRAME);
		Mat result=imgCapColor.clone();
		for (PicStats p : objects){
			cur=p;
			keyPoints=p.getKeyPoints();
			descriptor=p.getDescriptor();
			processFrame(capture);
			if (maxPoints < cur.getNumOfMatches()){
				bestKeyPoints = cur.getPointMatches();
			}
		}
		
		imgCapColor=result;
		drawOneCircleFromPoints(bestKeyPoints);		
		
		Utils.matToBitmap(imgCapColor,frameBmp);

		return frameBmp;
	}

	@Override
	protected Bitmap processFrame(VideoCapture capture) 
	{
//		getCurrentObject();
		getCurrentObjectFast();
		//put this in a remark when using a few pics per frame
		
		if (capture == null)
			return null;
			
		capture.retrieve(imgCapColor, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
		capture.retrieve(imgCap, Highgui.CV_CAP_ANDROID_GREY_FRAME);

		frameBmp = Bitmap.createBitmap(imgCapColor.cols(), imgCapColor.rows(), Bitmap.Config.ARGB_8888);
		Utils.matToBitmap(imgCapColor,frameBmp);
				
		detector.detect(imgCap, keyPointsCap);
		extractor.compute(imgCap, keyPointsCap, descriptorCap);
		
		if (descriptorCap.empty() || descriptor.empty())
			return frameBmp;
		
		
		matcher.knnMatch(descriptor, descriptorCap, matches, 2);	
		
		goods.clear();
		
		for (int i=0; i<Math.min(descriptor.rows()-1, matches.size()); i++)
		{
			point=matches.get(i).toArray();
			if (((point.length==2) && (point[0].distance < knnMatchingThreshold * (point[1].distance)))){				
				goods.add(point[0]);		
			}
		}
		
		matches.clear();
		
		matcher.knnMatch(descriptorCap, descriptor, matches, 2);	//TODO sometimes crushes see why...
		
		goods_2.clear();
		
		for (int i=0; i<Math.min(descriptor.rows()-1, matches.size()); i++)
		{
			point=matches.get(i).toArray();
			
			if (((point.length == 2) && (point[0].distance < knnMatchingThreshold * (point[1].distance)))){
				goods_2.add(point[0]);
			}
		}
		
		if (isSym)
			return symetricSearch(goods,goods_2,keyPointsCap.toArray(),1);
		else
			return AsymetricSearch(goods,goods_2,keyPointsCap.toArray(),4);
	}
	
	private Bitmap symetricSearch(ArrayList<DMatch> l1,List<DMatch> l2,KeyPoint[] points,int thresh){
		
		ArrayList<KeyPoint> pointMatches = new ArrayList<KeyPoint>();
		
		for (DMatch d1 : l1){
			for (DMatch d2 : l2){
				if ((d1.queryIdx==d2.trainIdx) && (d2.queryIdx==d1.trainIdx)){
					pointMatches.add(points[d2.queryIdx]);	
				}
			}
		}
		
		int numOfMatches=pointMatches.size();
		cur.setNumOfMatches(numOfMatches);
		
		if (numOfMatches > thresh){
			notifySoundAlert();
			drawOneCircleFromPoints(pointMatches);
			drawCirclesFromPoints(pointMatches);
			cur.setPointMatches(pointMatches);
		}
		
		Core.putText(imgCapColor, "matches: " + numOfMatches +"\nkeypoints:" + keyPoints.total(),
				new Point(10, 100), 3, 1, new Scalar(255, 0, 0, 255), 2);
		
		Utils.matToBitmap(imgCapColor,frameBmp);
		return frameBmp;
	}
	
	private Bitmap AsymetricSearch(ArrayList<DMatch> l1,List<DMatch> l2,KeyPoint[] points,int thresh){
		
		ArrayList<KeyPoint> pointMatches = new ArrayList<KeyPoint>();
		
		for (DMatch d1 : l1){
			pointMatches.add(points[d1.trainIdx]);
		}
		
		for (DMatch d2 : l2){
			pointMatches.add(points[d2.queryIdx]);
		}
		
		int numOfMatches=pointMatches.size();
		cur.setNumOfMatches(numOfMatches);
		
		if (numOfMatches > thresh){
			notifySoundAlert();
			drawOneCircleFromPoints(pointMatches);
			drawCirclesFromPoints(pointMatches);
			cur.setPointMatches(pointMatches);
		}
		
		Core.putText(imgCapColor, "matches: " + numOfMatches +"\nkeypoints:" + keyPoints.total(),
				new Point(10, 100), 3, 1, new Scalar(255, 0, 0, 255), 2);
		
		Utils.matToBitmap(imgCapColor,frameBmp);
		return frameBmp;
	}

	
	private void drawCirclesFromPoints(List<KeyPoint> points){
		for (KeyPoint k : points){
            Core.circle(imgCapColor,k.pt,5,new Scalar(0, 255, 0));	
		}
	}


	private void drawCirclesFromPoints(Mat imgCapColor2, ArrayList<DMatch> goods, KeyPoint[] tkeyPointsCap,boolean isTrain) 
	{
		if (isTrain){
			for (DMatch dm : goods){
	            Core.circle(imgCapColor,tkeyPointsCap[dm.trainIdx].pt,5,new Scalar(0, 255, 0));				
			}		
		}
		else{
			for (DMatch dm : goods){
	            Core.circle(imgCapColor,tkeyPointsCap[dm.queryIdx].pt,5,new Scalar(255, 0, 0));				
			}		
		}
	}
	
	private void drawOneCircleFromPoints(List<KeyPoint> tkeyPointsCap) 
	{
		if (tkeyPointsCap.isEmpty())
			return;
		List<Double> xList=new ArrayList<Double>();
		List<Double> yList=new ArrayList<Double>();
		int size = tkeyPointsCap.size();
		double x,y;
		
		for (KeyPoint p : tkeyPointsCap){
            xList.add(p.pt.x);	
            yList.add(p.pt.y);	
		}
		
		Collections.sort(xList);
		Collections.sort(yList);
		
		if (size % 2 == 0){
			x = xList.get(size/2) + xList.get(size/2 - 1);
			x= x/2;
			y = yList.get(size/2) + yList.get(size/2 - 1);
			y= y/2;
		}
		else{
			x = xList.get(size/2);
			y = yList.get(size/2);
		}
		
		Core.circle(imgCapColor,new Point(x,y),50,new Scalar(255, 0, 0), 5);	
			
	}

	
	private void drawOneCircleFromPoints(ArrayList<DMatch> goods, KeyPoint[] tkeyPointsCap) 
	{
		List<Double> xList=new ArrayList<Double>();
		List<Double> yList=new ArrayList<Double>();
		int size = goods.size();
		double x,y;
		
		for (DMatch dm : goods){
            xList.add(tkeyPointsCap[dm.trainIdx].pt.x);	
            yList.add(tkeyPointsCap[dm.trainIdx].pt.y);	
		}
		
		Collections.sort(xList);
		Collections.sort(yList);
		
		if (size % 2 == 0){
			x = xList.get(size/2) + xList.get(size/2 - 1);
			x= x/2;
			y = yList.get(size/2) + yList.get(size/2 - 1);
			y= y/2;
		}
		else{
			x = xList.get(size/2);
			y = yList.get(size/2);
		}
		
		Core.circle(imgCapColor,new Point(x,y),50,new Scalar(255, 0, 0));	
	}

	//can be deleted (Asaf)
//	private void drawCirclesFromPointsKMeans(ArrayList<DMatch> goods, MatOfDMatch good_matches, KeyPoint[] tkeyPointsCap) {
//		// kmeans clustering
//		MatOfDMatch best = new MatOfDMatch();
//		TermCriteria c = new TermCriteria();
//		c.epsilon = 0.7;
//		Core.kmeans(good_matches, goods.size() / 2, best, c , 2, 0);
//		List<DMatch> dmatch = best.toList();
//		
//		double x=0;
//		double y=0;
//		for (DMatch dm : dmatch){
//			x+=tkeyPointsCap[dm.trainIdx].pt.x;
//			y+=tkeyPointsCap[dm.trainIdx].pt.y;
//		}
//		x=x / best.total();
//		y=y / best.total();
//        
//		
//		Core.circle(imgCapColor,new Point(x,y),50,new Scalar(0, 255, 0));			
//	}


	//can be deleted (Asaf)
//	private void drawRectFromPoints(Mat resImg, ArrayList<DMatch> goods, KeyPoint[] TkeyPointsCap) 
//	{
//		KeyPoint[] TkeyPoints =keyPointsCap.toArray();
//		List<Point> pointMatchesImg= new ArrayList<Point>();
//		List<Point> pointMatchesImgCap= new ArrayList<Point>();
//		MatOfPoint2f matchesImg= new MatOfPoint2f();
//		MatOfPoint2f matchesImgCap= new MatOfPoint2f();
//		for (DMatch dm : goods){
//			pointMatchesImg.add(TkeyPoints[dm.queryIdx].pt);
//			pointMatchesImgCap.add(TkeyPointsCap[dm.trainIdx].pt);				
//		}
//		
//		matchesImg.fromList(pointMatchesImg);
//		matchesImgCap.fromList(pointMatchesImgCap);
//					
//		Mat H;
//		H = Calib3d.findHomography(matchesImg,matchesImgCap,Calib3d.RANSAC,3);
//		
//		MatOfPoint2f cornersImg = new MatOfPoint2f(new Point(0,0),new Point(objectMat.cols(),0),new Point(objectMat.cols(),objectMat.rows()),new Point(0,objectMat.rows()));
//		MatOfPoint2f cornersImgCap = new MatOfPoint2f(new Point(),new Point(),new Point(),new Point());
//		Core.perspectiveTransform( cornersImg, cornersImgCap, H);
//		
//		Point[] cornersImgCapArr= cornersImgCap.toArray();
//		
//		//Draw lines between the corners (the mapped object in the scene image )
//		Core.line( resImg,new Point( objectMat.cols()+cornersImgCapArr[0].x, cornersImgCapArr[0].y),new Point( objectMat.cols()+cornersImgCapArr[1].x, cornersImgCapArr[1].y), new Scalar(0, 255, 0), 4 );
//		Core.line( resImg,new Point( objectMat.cols()+cornersImgCapArr[1].x, cornersImgCapArr[1].y),new Point( objectMat.cols()+cornersImgCapArr[2].x, cornersImgCapArr[2].y), new Scalar( 0, 255, 0), 4 );
//		Core.line( resImg,new Point( objectMat.cols()+cornersImgCapArr[2].x, cornersImgCapArr[2].y),new Point( objectMat.cols()+cornersImgCapArr[3].x, cornersImgCapArr[3].y), new Scalar( 0, 255, 0), 4 );
//		Core.line( resImg,new Point( objectMat.cols()+cornersImgCapArr[3].x, cornersImgCapArr[3].y),new Point( objectMat.cols()+cornersImgCapArr[0].x, cornersImgCapArr[0].y), new Scalar( 0, 255, 0), 4 );
//        
//	}


	// ======================= UI interaction methods ======================
	@Override
	public void notifySoundAlert() 
	{
//		if (soundPool != null)
//			soundPool.play(cur.getSoundId(), volume, volume, 1, 0, 1f);
		
		if (mps != null && !(mps.length == 0))
			mps[cur.getSoundId()].start();
	}

	@Override
	public void markRectangle(Rect r) {
//		Core.rectangle(mRgba, r.tl(), r.br(), new Scalar(0, 255, 0, 255), 3);
	}
	
	
	// ======================== initialization methods ===================
	private void initSound(Context context)
	{
		useSound = context.getSharedPreferences(Settings.settings, 0).getBoolean(Settings.useSoundPref, true);
		if (!useSound) return;
		
		AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
		float actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
		float maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
		volume = actualVolume / maxVolume;
		
		soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
	}
	
	private void initSound(Context context, int size) 
	{
		useSound = context.getSharedPreferences(Settings.settings, 0).getBoolean(Settings.useSoundPref, true);
		if (!useSound) return;
		
		AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
		float actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
		float maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
		volume = actualVolume / maxVolume;
		
		mps = new MediaPlayer[size];
	}
	
	private void loadSound(Context context, String path, int i) 
	{
		if (path != null)
			mps[i] = MediaPlayer.create(context, Uri.parse(path));
		else
			mps[i] = MediaPlayer.create(context, R.raw.sound);
	}

	
	private int loadSound(Context context, String path) 
	{
		if (path != null)
			return soundPool.load(path, 1);
		
		return soundPool.load(context, R.raw.sound, 1);
	}
	
	private void loadRefrenceObject(Context context, Bitmap bmp)
	{
		if (bmp == null)
			return;
		else
			refBmp = bmp;
			
		objectMat = new Mat();
		Mat imgColor = new Mat();
		Utils.bitmapToMat(refBmp, imgColor);
		Imgproc.cvtColor(imgColor, objectMat, Imgproc.COLOR_RGB2GRAY);
		
//		refBmp.recycle();
		
		if (objectMat.dataAddr() == 0){
			Log.e(TAG, "error reading object");
			return;
		}	
	}

	private void createDetectorSettingsFile(Context context) 
	{
		tempDir = context.getDir("temp", Context.MODE_PRIVATE);
		threshFile = new File(tempDir, "thresh.txt");
		FileWriter fw;
		try {
			fw = new FileWriter(threshFile);
			fw.write("%YAML:1.0\nhessianThreshold: " + hessianThreshold +
					".\noctaves: " + numOfOctaves +
					"\noctaveLayers: " + octaveLayers +
					"\nupright: 0\n");
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}	
	}
}
